"""
@-*- coding: utf-8 -*-
@ python：python 3.8
@ 创建人员：ylx
@ 创建时间：2025/2/19 下午4:59
@ 内容：学习装饰器、*args-**kwargs、解包，git初始化项目并推送，python的垃圾回收机制，迭代器，生成器
"""

"""
装饰器是python中一种非常强大的语法特性，用于修改或增强函数、方法或类的行为
无需直接修改源码，其本职来说是一个高阶函数，他可以接收一个函数作为参数，并返回一个新的函数

装饰器的核心思想是，在不改变原函数的定义情况下，动态的扩展或者修改函数的功能

1.python中，函数被视作一等公民，这意味着函数可以像普通变量一样被传递，赋值，并且返回
2.高阶函数是指可以接收函数作为参数或返回函数的   函数
3.不含参装饰器
4.含参装饰器-如果需要为装饰器本身传入参数，那你可以再嵌套一层函数
5.装饰器不含参，但是被装饰的函数含参
"""


# def bb():
#     print("456")
#
#
# def aa():
#     print("123")
#
#
# cc = bb   #将函数赋值给变量--1
# cc()    #输出bb函数原本的输出，456


# def bb(func):
#     func()
#
#
# def aa():
#     print("123")
#
#
# bb(aa)    #2


# def my_decorator(func):     #my_decorator是一个装饰器函数，不含参
#     def wrapper():  #wrapper是装饰器内部定义的新函数，它在调用原始函数func()的前后添加了额外的逻辑
#         print("123")
#         func()  #调用原始函数
#         print("456")
#
#     return wrapper  #这里返回的是一个函数，但是一定注意不要加括号
#
#
# @my_decorator   #语法糖，相当于say_hello = my_decorator(say_hello)
# def say_hello():
#     print("hello,world")
#
#
# say_hello()


# def repeat(n):  #repeat是一个带参数的装饰器工厂函数，这个例子是一个含参的装饰器，这个参数是带在装饰器本身上的，我们给之前
#     #不带参的装饰器外部嵌套一层，带参即可，并且我们需要分别返回里边的2层函数
#     def decorator(func):    #decorator是实际的装饰器函数
#         def wrapper(*args, **kwargs):   #wrapper是装饰器内部定义的新函数，负责重复调用原始函数
#             for _ in range(n):
#                 func(*args, **kwargs)
#         return wrapper
#     return decorator
#
#
# @repeat(3)
# def say_hello():
#     print('hello!')
#
#
# say_hello()


def decorator(func):
    def wrappers(*args, **kwargs):
        print(f"calling function '{func.__name__}' with arguments {args} and keyboard arguments {kwargs}")
        result = func(*args, **kwargs)
        print(f"function '{func.__name__}' returned {result}")
        return result

    return wrappers


# @decorator  #装饰器语法糖是python提供的一种简化写法，它的作用就是将函数作为参数传给这个装饰器，并且用
#装饰器返回的新函数代替原来的函数，在这个例子中就可以这样写 add = decorator(add),实际上最后add函数使用的是装饰器装饰过后的wrapper函数
#装饰器是否需要参数，取决于装饰器本身，如果按照我们提供的例子decorator不需要额外的参数，它只接收一个参数func,既被装饰的函数，直接使用即可
#如果需要额外的参数，需要给外部嵌套函数的方式来定义带参数的装饰器
def add(a, b):
    return a + b


add(3, 5)  #针对上述这个例子，我的被装饰函数含参，我为什么要在装饰器函数中传入*args和**kwargs


# 首先装饰器本身是一个函数，它接收另一个函数作为参数，并返回一个新的函数，它的目的是在不修改原函数代码的情况下，增加或者扩展原有函数的功能
# 在上述例子中，add函数是有默认2个入参的，使用装饰器时，其实就是将add函数传给了这个decorator，然后decorator返回了一个新的函数，wrappers 来替代原来的add,所以此时会变成wrappers(a,b),
# 装饰器的核心思想是通用性，让装饰器能用于任何函数，既无论这个函数有多少个参数，或者无参数，我们都需要让内部定义的wrappers函数 可以接收任意数量的入参 *args
# 是用于接收任意数量的位置参数，将其打包成一个元组/针对def时，如果在函数中使用，那么又是解包 **kwargs 用于接收任意数量的关键字参数，将其打包为一个字典
# 这样的设计好处就是，wrappers不需要提前知道被装饰函数的具体参数签名，增强了装饰器的通用性，可能A函数不需要参数，B函数需要2个参数，C函数需要5个参数等等
# wrapper这样写可以承接任意数量的入参，再注意内部使用其实还是给原始的func使用，而func中用时，又是解包，所以相当于通过wrapper中转了一下 从代码中来分析，调用add(3,5)时，这个add已经是经过装饰add,
# 实际就是wrappers,wrappers函数会被调用，实际就写成了wrapper(3, 5)，而对wrapper定义时，就明确了他可以接受任意数量的参数，目的就是在这里，
# *args接收到（3,5），**kwargs接收到{},因为没有关键字参数 然后wrappers再将这些参数通过func(*args,**kwargs)传递给原始的add函数
# 而在func中时，这些被打包的参数，又都会被对应的解包，从而正确触发原函数的逻辑
# 如果我们不使用上述方式，我们使用的是给wrappers定义好2个入参，如wrappers(a,b)，那么这个装饰器只能装饰有固定2个参数的函数，如果更换函数
# 参数数量增加或减少或没有，那这个装饰器无法工作 上述例子我们如何修改为一个带参数的装饰器，如下


def decorator_with_param(log_level):  #定义带参数的装饰器，这是一个返回装饰器的函数，它接收log_level作为参数，并且根据这个参数生成一个具体的装饰器，actual_decorator,
    #最终actual_decorator才是真正的装饰器，它接收被装饰的函数func
    def actual_decorator(func):  #实际的装饰器，接收被装饰的函数func
        def wrappers(*args, **kwargs):
            if log_level == 'verbose':
                print(f"verbose:calling function '{func.__name__}' with arguments {args} and kwargs {kwargs}")
            elif log_level == 'simple':
                print(f"simple:calling function '{func.__name__}'")

            result = func(*args, **kwargs)
            print(f"{func.__name__} return {result}")
            return result

        return wrappers

    return actual_decorator


@decorator_with_param('verbose')
def add(a, b):
    return a + b


add(3, 5)


#decorator可以和上面的decorator_with_param一样理解为返回装饰器的函数吗？
#返回装饰器的函数是指一个函数，本身并不直接作为装饰器，而是生成并返回一个真正的装饰器，这种设计是通常用于需要传递额外的参数给装饰器的情况
#decorator_with_param 的分析已经在上述例子中说明
#decorator，直接接收被装饰的函数func，并返回一个新的函数wrappers,它没有额外的参数，所以不需要外层嵌套一层函数，来生成装饰器
#我们将最纯粹的decorator，就是一种最简单的装饰器形式，可以直接称为装饰器
#对于decorator_with_param，它返回真正的装饰器函数actual_decorator，使用@decorator_with_param(arg)
#实际上是先明确@后面的函数到底是谁，如何处理，这种含参装饰器外层的先执行返回一个真正的装饰器，所以@真正的装饰器，你可以理解为我们之前这种不含参的装饰器的
#运行流程，只不过由于闭包原则，我们在里边的wrapper仍然可以使用最外层我们需要的参数
#对于decorator，它直接返回真正的装饰器wrappers，直接使用@decorator使用


#*args,**kwargs是python中用于函数定义和调用时处理参数的特殊语法，允许函数接收不定数量的位置参数，和关键字参数
#*args 接收任意数量的位置参数，将所有传入的位置参数打包为一个元组，**kwargs，接收任意数量的关键字参数，将传入的关键字参数打包成一个字典
#我们需要这样处理的原因是因为我们也不确认函数需要接收多少个参数，或者参数的具体形式，是位置还是关键字，我们使用这种方法，可以让函数更加的灵活
#如果我们没有传入任何位置参数，args将是一个空元组()
#如果没有传入任何关键字参数，kwargs将是一个空字典{}


def example_args(*args):
    print(type(args))
    print(args)


def example_kwargs(**kwargs):
    print(type(kwargs))
    print(kwargs)


example_args(1, 2, 3, 'hello')
example_kwargs(name='alice', age=18, school='nem_school')
example_kwargs()
example_args()


#*和**还可以在函数调用时来解包参数
def normal_function(aa, bb, cc, dd):
    print(f"a={aa},b={bb},c={cc},d={dd}")


args_tuple = (1, 2, 3)
normal_function(*args_tuple, 4)  #等价于normal_function(1,2,3,4)

kwargs_dict = {"cc": 30, "dd": 40}
normal_function(1, 2, **kwargs_dict)

#解包是用于将可迭代对象，如列表，元组，字典中的元素分配给多个变量的一个过程
#序列解包
a, b, c = [1, 2, 3]
print(a, b, c)
#元组解包
x, y = (4, 5)
print(x, y)
v, q = 7, 8
print(v, q)
#注意上述的解包的变量数量必须与序列长度匹配，否则会引发ValueError
#扩展解包，我们使用*，扩展解包允许我们捕获剩余的元素，或者解包时忽略某些元素
first, *middle, last = [1, 2, 3, 4, 5]  #捕获剩余元素
print(middle)   # 打印[2, 3, 4]
print(first, *middle, last)

#忽略中间元素
first, *_, last = [1, 2, 3, 4, 5]
print(first)
print(last)


#字典解包使用**，主要目的是用于将字典中的键值对传递给函数或合并字典
def greet(name, age):
    print(f"name{name}, age{age}")


person = {'name': "alice", "age": 25}
greet(**person)

#合并字典
dict_1 = {"a": 1, "b": 2}
dict_2 = {"c": 3, "d": 4}
merge_dict = {**dict_1, **dict_2}
print(merge_dict)


#函数参数解包
def add(a, b, c):
    return a + b + c


#位置参数解包
args = [1, 2, 3]
result_1 = add(*args)  #等价于add(1,2,3)
print(result_1)

#关键字参数解包
kwargs = {"a": 1, 'b': 2, "c": 3}
result_2 = add(**kwargs)
print(result_1)

#循环中解包，解包元组
data = [(1, 'apple'), (2, 'banana'), (3, "melon")]

for index, fruit in data:
    print(f"index{index},fruit{fruit}")

#忽略某些值
for _, aa in data:
    print(aa)

#git，进入到当前这个项目的文件夹下，首先git init
#git add .
#git commit -m ''
#git remote add 仓库名称 远程地址
#git push 仓库名称 master


#python的垃圾回收机制
#垃圾回收机制是内存管理中的重要组成部分，负责自动释放不再使用的对象所占用的内存
#从而避免程序的内存泄漏，提高程序的性能
"""
python主要采用引用计数和分代垃圾回收2种机制来实现垃圾回收。首先垃圾回收是指程序运行时，自动释放不再使用的对象所占用的内存
在python中，垃圾回收主要由python解释器内置的垃圾回收器，GC来负责
python的垃圾回收机制主要是2种，引用计数和分代垃圾回收

引用计数是最基本的垃圾回收机制，python使用它来跟踪每个对象的引用次数，每个对象都有一个引用计数器
，记录指向该对象的引用数量，当引用计数器为0时，说明该对象不再被使用，python会立即释放该对象占用的内存


引用计数仅仅是统计这个对象被多少变量引用，而循环引用的层面是对象互相引用
比如说
a = [1,2,3]
b = a
对于列表对象 [1,2,3]  被变量a引用，然后b引用a 实际上对象也被b引用了
引用计数为2 
del a 那么引用计数变为1
del b 引用计数变为0 对象不在被引用，内存中这个对象将被垃圾回收

对于对象和对象之间的，我们通过变量 将这种引用最后实际落在 对象和对象之间的引用，且一定是一个双向的引用
而不是仅如A对象引用B对象，这样我们称为单向引用，是不会导致循环的，Python的引用计数可以处理这种
而如果是双向的 既A引用了B，B引用了A这种就必须要使用Python的分代回收机制，搭配标记清除算法完成垃圾回收

举一个生活中的例子来说明循环引用：
假设你和你的朋友一起去参加一个聚会，结束后你们都准备离开。但是，你们都有一个奇怪的习惯：
你 不会离开，除非你的朋友先离开。
你的朋友 也不会离开，除非你先离开。
结果是：
你等着你的朋友离开。
你的朋友也在等你离开。
最终，你们两个都卡在那里，谁也无法离开。
这就是一个典型的“循环引用”问题！

class Person:
    def __init__(self, name):
        self.name = name
        self.partner = None  # 每个人的伙伴

# 创建两个对象
alice = Person("Alice")
bob = Person("Bob")

# 让 Alice 和 Bob 互相成为伙伴
alice.partner = bob
bob.partner = alice

# 现在 Alice 和 Bob 形成了循环引用
print(alice.partner.name)  # 输出: Bob
print(bob.partner.name)    # 输出: Alice

即使我们删除了对 alice 和 bob 的外部引用（比如 del alice 和 del bob），但是alice 和 bob 所代表的实际的类实例-仍然互相引用。
因此，它们的引用计数不会降为 0，垃圾回收器无法直接回收它们。

总结
循环引用 就像两个人互相等待对方先离开，导致谁都无法离开。
在编程中，循环引用会导致对象无法被垃圾回收器清理。
解决方法包括手动断开引用或使用弱引用来打破循环。

使用weakref模块创建若引用，避免强引用导致循环
使用weakref.ref() 方法，比如
# 使用弱引用来避免循环引用
alice.partner = weakref.ref(bob)
bob.partner = weakref.ref(alice)
在这种情况下，即使 alice 和 bob 互相引用，弱引用不会增加引用计数，因此垃圾回收器仍然可以正确回收它们。


单向引用-
class Node:
    def __init__(self, name):
        self.name = name
        self.next = None

# 创建两个对象
node_a = Node("Node A")
node_b = Node("Node B")

# 单向引用
node_a.next = node_b
初始引用计数：
node_a 的引用计数是 1（因为我们通过 node_a 引用了它）。
node_b 的引用计数是 2：
1 来自 node_b 本身的引用。
1 来自 node_a.next 对它的引用。
删除外部引用：
Python
深色版本
del node_a
del node_b
删除过程：

删除 node_a 后，node_a 的引用计数降为 0，node_a 被回收。
回收 node_a 后，node_b 的引用计数从 2 减少到 1（因为 node_a.next 的引用消失了）。
删除 node_b 后，node_b 的引用计数降为 0，node_b 被回收。
结果：
所有对象都被正确回收，没有任何内存泄漏。
单向循环如果先删除b 再删除a 
删除 node_b：
node_b 的引用计数从 2 减少到 1（因为外部引用 node_b 被删除）。
此时，node_b 仍然被 node_a.next 引用，因此不会被回收。
删除 node_a：
node_a 的引用计数降为 0，node_a 被回收。
回收 node_a 后，node_b 的引用计数从 1 减少到 0（因为 node_a.next 不再存在）。
此时，node_b 的引用计数降为 0，node_b 被回收。
"""

import sys

a = [1, 2, 3]  #创建列表对象，引用计数为1
b = a  #再创建一个引用，计数为2

print(sys.getrefcount(a))  #统计引用计数，实际getrefcount自身也会被增加进来
del b  #删除引用b,引用-1
del a  #删除引用a.引用为0，对象被销毁


#引用计数的优点简单高效，及时释放不再使用的对象
#缺点，无法处理循环引用，循环引用就是指2个或多个对象相互引用，形成一个闭环，导致他们的引用计数始终大于0，即使实际上这些对象已经不可到达
#引用计数的核心思想是通过跟踪对象的引用次数来判断对象是否可以立即被销毁
#然而循环引用，即使这些对象实际上已经不再被程序使用，但是他们之间相互引用，导致计数始终大于0，从而无法触发回收机制
#即使你删除外部对ab的引用，但是这2个对象仍然互相引用，导致它们的引用计数不为0

#举例
class MyClass:
    def __init__(self, name):
        self.name = name
        self.ref = None

    # 如何解决  给类中增加方法
    def clear(self):
        self.ref = None


# 创建2个节点
a = MyClass('a')
b = MyClass('b')

# 形成循环引用
a.ref = b
b.ref = a

# # 处理方案-手动断开引用
# a.clear()
# b.clear()


# 删除对a 和 b 的应用或者称为删除对象
del a
del b
# 上述例子中  a 属性引用了b  b属性引用了a
# 即使你删除了对 a b 的引用，但是这2个对象仍然会因为循环引用而无法被垃圾回收器释放







a = []
b = []
a.append(b)
b.append(a)
del a
del b


#为了弥补引用计数的不足，python实现了分代的垃圾回收机制
#将对象分为三代，新建对象被分在0代
#如果对象能在内存回收中幸存下来，它会被移动至更高一代
#高代的对象被认为更加的持久，因此检查频率低，从而优化性能
#开发者可以通过gc模块手动触发垃圾回收
#开发者如何优化垃圾回收，那就减少不必要的引用，避免创建过多的循环结构，使用del删除不再需要的变量
#调整垃圾回收参数，使用gc模块调整垃圾回收的阈值，或禁用垃圾回收
#为什么分代回收可以解决循环引用问题？
#分代回收基于以下假设，新生对象更容易变成垃圾，老对象，存活时间长更有可能继续存储下去
#python会定期扫码每一代中的容器对象，如列表，字典，容器对象是指可以包含其他对象引用的对象
#扫描过程中，分代回收机制会检查是否存在循环引用，对检测到的循环引用对象，分代回收会减少它们的引用计数，直到这些对对象的引用计数变为0
#分代回收通过动态调整每一代的回收阈值，避免频繁扫描所有对象，从而提升效率
#
#
#
# 如果发现某个对象是活动对象，但是所有的引用都指向自身或后代，则认为这是一个循环引用，使用
#标记清楚算法，标记所有从根对象可达的对象为存活，清除未被标记的对象，释放他们占用的内存
#总的来说就是分代回收通过定期扫码容器对象，结合标记清楚算法，及动态调整回收频率，解决循环引用问题


#标记-清除算法是一种独立的垃圾回收策略，用于检查和回收不可达对象，分代回收机制中，这个通常被用来处理那些
#无法通过简单引用计数解决的问题，如循环引用，分代回收不是一种具体的垃圾回收算法，而是一种优化策略
#将对象分为不同的代，根据对象存活的时间调整垃圾回收频率，结合多种垃圾回收算法，如引用计数，标记清除等来提高效率
#分代回收如何结合2者，对于新生代，新创建的对象，python主要依赖引用计数进行垃圾回收，因为新生代对象的生命周期短，引用关系简单
#随着对象存活时间增加，移动至老年代，在老年代引用关系可能更加的复杂，python会定期的使用标记清楚算法扫描这些对象，以检测和回收不可达对象，包括循环引用

#在垃圾回收中，判断一个对象是否可以被回收的核心依据就是可达性，如果一个对象可以通过根对象，如全局变量，栈帧中的局部变量
#递归访问到，则该对象被认为是可达的，不能被回收，相反如果一个对象无法通过任何路径从根对象访问到，则对象被认为不可达，可以被回收


#yield的基本介绍
#在python中yield关键字主要用于生成器函数中，目的就是使得函数能够像迭代器一样工作，函数可以被遍历，但是不会一次性将所有结果都加载到内存中
#yield关键字可以用来创建生成器函数，生成器函数与普通函数不同，它不会一次性计算出所有结果，生成器函数执行时会返回一个生成器对象
#不是普通函数那样的一个值，而是每次被调用时，生成一个值，从而实现惰性求值，这种特性使得yield特别适合处理大量数据，或者实现流式处理
#生成器函数的特点
#惰性求值-生成器每次调用时生成一个值，而不是一次性生成所有的值，可以节省内存
#状态保持，生成器函数会记住上一次返回值时的状态，下次迭代会从上次停止的地方继续执行
#无限序列-生成器可以用来生成无限序列，而不会占用大量内存
#定义生成器函数
#带有yield的函数在python中被称之为生成器generator，也就是说，当你调用这个函数的时候，函数内部的代码并不是立即执行
#这个函数只返回一个生成器，或者我们称其为生成器对象
#举例
# def generator():
#     for i in range(10):
#         yield i * 1
#
#
# gen = generator()
# print(gen)---这个输出并不会输出一个值，而是打印生成器对象本身
#生成器函数使用yield关键字来生成值，而不是return，并借助next()
#上述我们使用
#print(next(gen))就会输出1
#再执行2次print，会一次输出2、3，在这个实例中，simple_generator函数是一个生成器函数，依次
#生成123，next函数用于获取生成器的下一个值，函数第一次调用next(gen)函数时，generator执行到第一个yield，并返回yield后的值
#所以我们可以使用for循环来遍历生成器函数，依次打印出相关的值
#生成器表达式
#gen_expr = (x * x for x in range(5))
# for value in gen_expr:
#     print(value)

#我们还可以使用yield实现协程，这是更加高级的生成器，允许生成值的同时接收外部数据，使用send方法可以像生成器函数
#发送数据，并在生成器内部使用yield表达式接收数据
# def coroutine():
#     while True:
#         received = yield
#         print(f"接收到的数据: {received}")
#
#
# co = coroutine()
# next(co)  # 预激生成器
# co.send(10)  # 输出: 接收到的数据: 10
# co.send(20)  # 输出: 接收到的数据: 20
#如果我们没有预激生成器，那么后续执行send方法都会报错，主要原因还是生成器本质，yield表达式用于定义一个生成器函数，
#当生成器函数第一次被创建时，它实际上并没有被执行，我们可以理解它实际上是在def coroutine()，为了使得生成器开始执行
#并且到达第一个yield表达式，我们需要使用next()来触发一次，我们称这个过程为预激生成
#目的就是触发生成器从头开始执行，直到遇到第一个yield表达式为止
#对于yield的行为，它会暂停执行，返回控制权给调用者，然后执行后续send方法时可以接收参数
#如果你觉得手动触发一次next()非常的麻烦，你可以通过装饰器函数来完成这个步骤
def generator(func):
    def wrapper(*args, **kwargs):
        gen = func(*args, **kwargs)
        next(gen)
        return gen

    return wrapper()


@generator
def coroutine():
    while True:
        received = yield
        print(f"接收到的数据: {received}")


co = coroutine()  #使用装饰器后，无需调用next，因为装饰器函数已经帮你完成了
co.send(10)

#分析，为何在装饰器中，我们仍然需要返回result，而不是直接返回我们的装饰器函数或者直接返回wrappers
#首先装饰器的本质是一个高阶的函数，接收一个函数作为参数，返回一个新的函数，这个新的函数通常是包装函数。
#装饰器的作用是对原始函数的功能进行增强或者修改，同时我们还需要保证原有函数的调用方式不变
#在标准的装饰器模版中，decorator是装饰器函数，wrapper是包装函数，会在调用原始函数之前和之后执行额外的操作
#最后由decorator返回wrapper函数
#至于我们为什么要返回result，要确保装饰后函数的行为一致，因为你使用装饰器时，你肯定希望装饰后的函数仍然能像装饰前的
#函数一样正常工作，比如说，原始函数会返回结果，那么装饰后的函数也应该返回结果
#使用装饰器时就应该确保，原始函数的功能被正确执行，原始函数的返回值能够正确传递给调用者
#所以我们使用装饰器时，对于包装函数中，我们需要显示的返回被装饰函数的结果
#在python中，函数的返回值不会自动的传递给调用者，即使你在wrapper中确实调用了被装饰的函数
#但是只要wrapper没有显示的返回func的结果，那么调用者无法获取到原始函数的返回值，换个理解
#func的确会被执行，但是它的返回值仅仅保留再wrapper内部，而不会传递给调用者
#所以我们如果要让调用者收到func的值，那就要显示的返回func，这样的话，当我们调用装饰后的函数时
#返回值会正确的传递给调用者

# @decorator
# def add(a, b):
#     return a + b
#
# result = add(3, 5)
# print(result)  # 输出应该是 8
# 如果没有返回 result，而是直接返回 wrappers，那么 add(3, 5) 将不会返回计算结果 8，
# 而是返回 None（因为默认情况下，函数如果没有显式返回值，默认返回 None）


#生成器可以实现无限序列
#生成器适合生成无限序列，因为它们只在需要时生成值，不会占用大量的内存
# def infinite_sequence():
#     num = 0
#     while True:
#         yield num
#         num += 1
#
#
# gen = infinite_sequence()
#
# for i in range(10):
#     print(next(gen))
#这段代码核心我们要理解生成器的工作机制，首先这是一个被定义好的生成器函数，它使用了yield关键字
#yield的作用，当生成器函数被调用时，比如gen = inifinite_sequence(),它并不会立即返回生成器函数体中的代码，而是返回一个生成器对象
#我们需要每次调用next()方法时，生成器会从上次暂停的地方继续执行，直到遇到下一个yield表达式为止
#在yield处，生成器会将当前的值传递给调用者，并暂停执行
#我们搭配for循环和next()来调用生成器，每次调用时，生成器都会从上次暂停的地方继续执行，直到遇到下一个yield
# 第一次调用 next(gen)：
# 生成器开始执行，num = 0。
# 执行到 yield num，生成器返回 num 的当前值（即 0），然后暂停。
# 第二次调用 next(gen)：
# 生成器从暂停的地方继续执行，即执行 num += 1，此时 num 变为 1。
# 然后再次遇到 yield num，生成器返回 num 的当前值（即 1），然后暂停。
# 后续每次调用 next(gen) 都会重复上述过程，num 不断递增。
#num+=1，仅会在下次的for循环中调用next()时执行
# 当生成器暂停在 yield 处时，num += 1 并不会立即执行。
# 只有当调用 next(gen) 时，生成器才会从暂停的地方继续执行，先执行 num += 1，然后再继续运行到下一个 yield。

#生成器使用常见的场景
#使用生成器读取大文件可以避免将整个文件加载到内存中去，从而节省内存
# def read_large_file(file_path):
#     with open(file_path, 'r') as file:
#         while True:
#             line = file.readline()
#             if not line:
#                 break
#             yield line
#

#优化写法：
# def read_large_file(file_path):
#     with open(file_path, 'r') as file:
#         for line in file:  # 使用内置迭代机制
#             yield line
# #直接利用文件对象的可迭代性


# for line in read_large_file('large_file.txt'):
#     print(line, end='')
#这个示例中生成器逐行读取大文件，使用for循环打印每行内容
# 打开指定路径的文件（file_path）。
# 使用 while True 循环逐行读取文件内容，每次读取一行并返回（yield line）。
# 如果读取到空行（即文件结束），跳出循环。
#使用for循环遍历生成器，首先read_large_file('large_file.txt')这是一个生成器对象
#for循环遍历生成器时，for循环会自动的调用生成器的__iter__()和__next__()方法
#获取生成器返回的每一行数据，并赋值给line，
#打印每一行，end=''用于避免额外的换行符，因为print会在末尾添加换行，默认添加，而文件中的每行已经自己自带换行符了
#为什么for循环可以遍历这个方法？
#在python中，for循环可以用来遍历任何实现了可迭代协议的对象，生成器正是这种对象之一
#首先-什么是可迭代协议，一个对象，如果实现了__iter__()方法或者通过某种方式生成一个迭代器，就可以被for循环遍历
#生成器对象实现了__iter__()和__next__()方法，因此可以直接被for循环使用
#综上所述，结合生成器的工作机制
#我们调用生成器函数时，首先会生成一个生成器对象，而不是立即执行函数体中的代码，for循环迭代时
#生成器从上次暂停的地方继续执行，直到遇到下一个yield表达式，在yield处，生成器返回当前的值，并暂停执行，等待下一次迭代

#上述代码中，我们看到if not  line
#首先line = file.readline()用于从文件中读取一行的内容，返回该行的内容字符串，含换行符号反斜杠n
#如果文件已经读到末尾，那么在python中readline()会返回一个空字符串
#空字符串视为布尔值为假的值，非空字符串视为布尔值为真值，所以就是if not False-就是if True
#然后执行其中的break，跳处当前的while循环

#生成器还能实现管道处置，每个生成器函数负责一个处理步骤
# def pipeline_stage1(data):
#     for item in data:
#         yield item * 2
#
#
# def pipeline_stage2(data):
#     for item in data:
#         yield item + 1
#
#
# data = range(5)--既data = [0,1,2,3,4]
# stage1 = pipeline_stage1(data)
# stage2 = pipeline_stage2(stage1)
#
# for value in stage2:
#     print(value)
#分析-我们首先定义了2个生成器函数，stage1接收一个可迭代对象data，将每个元素*2，通过yield返回
#stage2，接受一个可迭代对象，data，每个元素+1返回
#首先我们给一个data= range(5),stage1=pipline_stage1(data),传入后返回一个生成器对象stage1
#stage2 = pipline_stage2(stage1),返回生成器对象stage2
#然后我们遍历stage2，打印结果
#生成器的一个重要特性就是懒加载
#虽然我们在这段代码中没有显示的调用next()来预激生成器，但是for循环会自动调用生成器的__iter__()和__next__()方法
#for循环触发生成器执行-进入stage2时
# for 循环触发生成器执行：
# 当进入 for value in stage2: 时，stage2 的 __iter__() 方法被调用，返回生成器本身。
# 每次迭代时，stage2 的 __next__() 方法被调用，生成器从上次暂停的地方继续执行。
# 生成器链的执行过程：
# stage2 的 __next__() 方法会调用 stage1 的 __next__() 方法。
# stage1 的 __next__() 方法会从 data 中获取下一个值，并计算 item * 2。
# 计算结果传递给 stage2，stage2 再计算 item + 1，并将最终结果返回给 for 循环。
# 生成器是否需要预激？
# 在这个例子中，虽然我们没有显式调用 next() 预激生成器，但 for 循环会自动处理生成器的迭代，因此代码仍然会正常执行。
# 生成器的懒加载特性：
# 生成器只有在被迭代时才会执行其内部代码。
# 多个生成器可以组成一个管道（pipeline），每个生成器只在需要时计算下一个值。

#为什么遍历stage2时stage1会被执行
# 生成器链的关系
# stage2 的输入是 stage1 的输出。
# 当我们遍历 stage2 时，stage2 需要从 stage1 获取数据。
# 因此，stage1 必须被执行以提供数据给 stage2。

#  生成器链的依赖关系
# 在这个例子中，stage2 的输入依赖于 stage1 的输出。
# 当 stage2 的 __next__() 方法被调用时，它需要从 stage1 获取下一个值。
# 为了获取这个值，stage2 会自动调用 stage1 的 __next__() 方法。
# 这种依赖关系会沿着生成器链逐层传递，直到到达最底层的数据源（在这个例子中是 data = range(5)）。
# 3. 执行流程分析
# 假设 data = range(5)，即 [0, 1, 2, 3, 4]，以下是代码的执行过程：
#
# (1) 初始化阶段
# data = range(5)：创建一个从 0 到 4 的可迭代对象。
# stage1 = pipeline_stage1(data)：创建生成器 stage1，但不会立即执行 pipeline_stage1 的代码。
# stage2 = pipeline_stage2(stage1)：创建生成器 stage2，同样不会立即执行 pipeline_stage2 的代码。
# (2) 遍历 stage2
# 当进入 for value in stage2: 时，stage2 的 __iter__() 方法被调用，返回生成器本身。
# 每次迭代时，stage2 的 __next__() 方法被调用。
# (3) 生成器链的执行过程
# 第一次迭代：
# stage2.__next__() 被调用。
# stage2 需要从 stage1 获取下一个值，因此调用 stage1.__next__()。
# stage1.__next__() 被调用后，stage1 从 data 中获取第一个值 0，并计算 0 * 2，得到 0。
# stage2 接收到 0，并计算 0 + 1，得到 1。
# 最终，1 被返回给 for 循环。
# 第二次迭代：
# stage2.__next__() 被调用。
# stage2 再次调用 stage1.__next__()。
# stage1.__next__() 从 data 中获取第二个值 1，并计算 1 * 2，得到 2。
# stage2 接收到 2，并计算 2 + 1，得到 3。
# 最终，3 被返回给 for 循环。
# 后续迭代：
# 类似地，每次 stage2.__next__() 被调用时，都会依次调用 stage1.__next__() 来获取下一个值。

# 4. 总结
# 生成器链的依赖关系：
# stage2 的执行依赖于 stage1 的输出。
# 当 stage2 的 __next__() 方法被调用时，它会自动调用 stage1 的 __next__() 方法。
# 这种依赖关系会沿着生成器链逐层传递，直到到达最底层的数据源。
# 生成器的懒加载特性：
# 生成器只有在被迭代时才会执行其内部代码。
# 在这个例子中，stage1 的执行是由 stage2 的需求驱动的。
# 通过这种机制，多个生成器可以组成一个高效的管道（pipeline），每个生成器只在需要时计算下一个值，从而节省内存和提高性能。

#
# 4.1 使用try-finally块进行清理
#
# 在生成器中，可以使用try-finally块确保在生成器结束时进行必要的清理操作。
# def generator_with_cleanup():
#     print("生成器开始")
#     try:
#         yield "Hello"
#     finally:
#         print("生成器结束")
#
#
# gen = generator_with_cleanup()
# print(next(gen))
# gen.close()  # 触发 finally 块


# 生成器中可以使用try-except块处理异常。
# def generator_with_exception_handling():
#     try:
#         yield 1
#         yield 2
#         raise ValueError("发生错误")
#         yield 3
#     except ValueError as e:
#         print(f"捕获异常: {e}")
#
#
# gen = generator_with_exception_handling()
#
# for value in gen:
#     print(value)


#yield的基本上用法
#def simple_generator():
#     yield 1
#     yield 2
#     yield 3
# gen = simple_generator()
# print(next(gen))  输出1
# print(next(gen))  输出2
# print(next(gen))  输出3
#使用for循环遍历生成器
#生成器的对象是可迭代的，因此我们可以使用for循环来遍历生成器生成的值

# for value in gen:
#     print(value)
#
# for value in simple_generator():
#     print(value)


#生成器转列表
#def simple_generator():
#     yield 1
#     yield 2
#     yield 3
#gen = simple_generator()
#print(list(gen))
# list(gen)：list() 函数会迭代生成器 gen，并将生成器产生的所有值收集到一个列表中。
# (1) 初始化阶段
# gen = simple_generator()：创建生成器对象 gen，但生成器尚未开始执行。
# (2) 调用 list(gen)
# 当 list(gen) 被调用时，list() 会不断调用生成器的 __next__() 方法，直到生成器耗尽。
# # (3) 生成器的执行过程
# 第一次调用 gen.__next__()：
# 执行到第一个 yield 1，生成器返回 1 并暂停。
# 第二次调用 gen.__next__()：
# 继续执行，遇到第二个 yield 2，生成器返回 2 并暂停。
# 第三次调用 gen.__next__()：
# 继续执行，遇到第三个 yield 3，生成器返回 3 并暂停。
# 第四次调用 gen.__next__()：
# 生成器没有更多的 yield 表达式，抛出 StopIteration 异常。
# list() 捕获到这个异常后停止迭代。
# (4) 收集结果
# list() 将生成器返回的所有值 [1, 2, 3] 收集到一个列表中。


"""
@-*- coding: utf-8 -*-
@ python：python 3.8
@ 创建人员：ylx
@ 创建时间：2025/4/25 下午3:45
@ 内容：编写简单的装饰器及生成装饰器的函数
"""
"""
为什么要使用装饰器：
1.使用装饰器可以提升代码复用，避免重复冗余代码
如果你有多个函数需要测量运行时间，可以直接将装饰器应用在这些函数上
而不是给函数增加上一样的代码，这样代码冗余，后续不好维护

2.使用装饰器可以保证函数的逻辑清晰，如果一个本身就很复杂的函数，我还要通过修改函数内部的代码
来测量运行时间，这样会模糊函数自身的主要逻辑，同时软件开发的原则就是单一职责
既一个函数只应该承担一项责任

3.通过装饰器，我们可以扩展别人写好的函数，可以添加额外行为，而不需要修改别人的源代码
比如说你正在使用一个第三方库的函数，需要添加额外行为，比如说给函数运行时间进行测量，那你就可以使用
装饰器

理解装饰器的运行流程，尤其是含参装饰器的运行流程，对下面-嵌套函数的运行流程需要掌握

嵌套函数形如
def out_function():
    print(1)

    def inner_function():
        print(2)

    print(3)
    inner_function()


外部函数 out_function 是顶层函数，可以直接被调用
它包含了一个嵌套的内部函数 inner_function

内部函数 inner_function  定义在 外部函数的  内部作用域 内
它只能在外部函数的  内部 被调用，不能直接被从外部访问

如果我现在调用
out_function() 它的执行流程如下
先执行 print(1)
然后 定义内部函数 inner_function  此时不会执行，因为此时仅仅是定义了函数

然后 print(3)
然后调用 inner_function() 此时这个内部函数才会被执行  执行其  print(2)

此处一定要注意 这个内部函数 如果没有被调用 实际上是不执行的，仅仅完成定义
在外部函数的 内部调用 内部函数后，Python会跳转到内部函数的定义并执行代码，
内部执行完成后，控制权返回到外部函数，若外部函数没有其他代码，则外部函数执行结束

作用域规则
内部函数的作用域，内部函数可以访问外部函数的变量  称为闭包
外部函数无法访问内部函数的变量，内部函数的变量仅在自身作用域内有效

闭包：若内部函数引用了外部函数的变量，并且外部函数返回了内部函数，
那么内部函数就会携带这些变量的引用 形成一个闭包
def outer_function():
    x = "local to outer"

    def inner_function():
        print(f"Inner: {x}")

    return inner_function

closure = outer_function()  # 返回内部函数
closure()  # 调用内部函数--既执行内部函数

上述-即使 outer_function 已经执行完毕，inner_function 仍然可以访问外部函数的变量 x
看起来outer_function 仅仅是执行了一个返回 inner_function的操作
但是这样的操作，就可以使得 inner_function() 仍然可以访问 外部函数的变量x

嵌套函数的用途：
1.封装逻辑：
嵌套函数可以将辅助逻辑封装在外部函数中，避免污染全局命名空间。
def calculate_area(radius):
    def validate_radius(r):
        if r < 0:
            raise ValueError("Radius cannot be negative")
        return r

    radius = validate_radius(radius)
    return 3.14 * radius ** 2

print(calculate_area(5))  # 输出：78.5

2.工厂函数-嵌套函数可以用来动态的生成函数
def power_generator(n):
    def nth_power(x):
        return x ** n

    return nth_power

square = power_generator(2)  # 生成平方函数
cube = power_generator(3)    # 生成立方函数

print(square(4))  # 输出：16
print(cube(3))    # 输出：27




嵌套函数的执行流程：
外部函数首先执行自己的显示代码。
内部函数只有在被显式调用时才会执行。
内部函数可以访问外部函数的变量，但外部函数无法访问内部函数的变量。


普通装饰器
def my_decorator(func):  # func 是被装饰的函数
    def wrapper(*args, **kwargs):
        print("Before function call")
        result = func(*args, **kwargs)
        print("After function call")
        return result
    return wrapper

@my_decorator
def say_hello():
    print("Hello!")

say_hello()
执行流程：
@my_decorator 等价于 say_hello = my_decorator(say_hello)。

自己理解-那这样的话由于我们上面学习了闭包的知识，此时我调用my_decorator()时将我之前需要被装饰的
func传递进入，这个方法会返回一个wrapper函数，请注意  相当于我的say_hello = wrapper函数了
此时我继续执行say_hello()时-等于触发了wrapper()，这也是闭包的原理，导致我可以接收外层函数的返回，并由此来触发外层函数
内部的 被定义的函数，故由此完成了简单的装饰器的 运行流程

my_decorator 接收 func（即 say_hello），并返回 wrapper。
调用 say_hello() 实际上调用的是 wrapper。
在这种情况下，func 是直接传递给装饰器函数的。



生成装饰器的函数，既你的装饰器需要支持参数，你想根据不同的参数动态的生成不同的装饰器行为，这时需要引入一层额外的函数
def decorator_factory(param):
    def actual_decorator(func):  # func 是被装饰的函数
        def wrapper(*args, **kwargs):
            print(f"Decorator parameter: {param}")
            print("Before function call")
            result = func(*args, **kwargs)
            print("After function call")
            return result
        return wrapper
    return actual_decorator

@decorator_factory(param="example")
def say_hello():
    print("Hello!")

say_hello()

执行流程：
语法糖展开-say_hello = decorator_factory(param="example")(say_hello)
这里有两个步骤：
调用最外层函数 decorator_factory(param="example")。
将返回值（即 actual_decorator）作用于 say_hello
先调用最外层函数时，返回了真正的装饰器函数
此时相当于 返回的真正的装饰器函数 作用于 我的func,指需要被装饰的函数--可以写为 actual_decorator(func)现在开始被调用
那执行这个就会返回 wrapper函数--至此 原来的 say_hello = wrapper() 所以，你执行say_hello(),本质上相当于你触发执行了
wrapper()函数，由于Python的闭包机制，导致你的wrapper可以访问外层函数的变量，从而完成，按照参数生成装饰器的需求


至此相关理解就成了简单装饰器，至于为什么参数可以被使用，这还是闭包的原理，导致内部函数可以访问
外部函数作用域内的变量
上述的例子是直接在外部函数 内 写了一个 a = "xxx",那如果能这样
就相当于我可以给外部函数传递一个变量，那内部的函数也是可以使用的




调用 decorator_factory(param="example")：
decorator_factory 接收到参数 param="example"。
返回内部的 actual_decorator 函数。
调用 actual_decorator(say_hello)：
actual_decorator 接收到被装饰的函数 func=say_hello。
返回 wrapper 函数。
替换原始函数：
say_hello 被替换为 wrapper 函数。
此时，调用 say_hello() 实际上是在调用 wrapper。


为什么上述这种使用语法糖时，func没有直接先传递给外部函数呢？
语法糖的要求
Python 的装饰器语法糖（@decorator）要求装饰器本身是一个可以接受函数作为参数的函数。具体来说：
如果装饰器不带参数（普通装饰器），语法糖会将被装饰的函数直接传递给装饰器。
如果装饰器带参数（生成装饰器的函数），语法糖会先调用最外层函数（decorator_factory），然后将被装饰的函数传递给中间层函数（actual_decorator）。



如果的被装饰函数是含参数的，那么我们怎么处理？
其实和简单的装饰器一样，如果你仅仅是被装饰的函数含有参数，那这个参数会被传递给wrapper，
wrapper最后又会吐给wrapper里的 func
这就理解为 你调用say_hello("liming")  实际上最后调用的是 wrapper("liming")，这个参数最后会被
wrapper里的func所使用

因为你的say_hello()  最后实际上是 say_hello = wrapper,所以最后触发的是wrapper()
而我们的wrapper(*args, **kwargs)是这样的写法，既wrapper函数是能够接受所有的关键字参数和位置参数的
首先我们来理解在定义函数中  使用*  ** 的作用
def sun(a, b): # 定义一个输入2个数，返回这2个数和的函数
    return sum(a,b)
    
print(sun(1, 2)) # 打印 3

那如果你想输入多个数字，来求和呢？比如说，你可能想求 1 2 3  或者 4 5 6 7 8那该如何解决或者优化上面的方法呢？
使用* 来处理不定长的位置参数
def sun(*args):
    return sum(args)


print(sun(1,2,3,4)) # 打印 10 
你传入的 1 2 3 4 会被*args 打包成一个元组 既 args = (1,2,3,4) 并将这个元组传入到函数中


同样如果你是不定长的关键字参数，那么使用**kwargs 会将其打包为-key/value 的字典，以kwargs = {key:value, key:value}的形式传递进入函数中
这样我们解决了函数中如何处理不定长位置参数  不定长关键字参数的问题

而如果我们将想被装饰的函数，修改为需要参数的函数时，且我们想写一个生成装饰器的函数，我们来看一下这个调用的流程

def time(times):
    def decorator(func):
        def wrapper(*args, **kwargs):
            if times > 2:
                print("introduce new student")
                result = func(*args, **kwargs)
                print("very,good")
            else:
                result = func(*args, **kwargs)
                
            return result
        return wrapper
    return decorator

@time(1)
def say_hello(name,school="small"):
    return print(f"hello my name is {name},my school is {school}")


say_hello("liming", school="big")

这段代码的执行流程
首先我们给say_hello函数增加1个time装饰器生成函数，say_hello函数本身接受一个位置参数。一个关键字参数
触发say_hello("liming", school="big") 函数时,本质上触发的是wrapper("liming", school="big")
在 Python 中，当使用装饰器时，被装饰函数的所有参数（包括位置参数 *args 和关键字参数 **kwargs）会自动传递给由装饰器返回的包装函数（即 wrapper 函数）
这是 Python 装饰器设计的一部分，目的是确保装饰器不会干扰原函数的正常调用方式。
1.参数传递的基本原理
当你定义一个装饰器并应用到某个函数上时，Python 实际上是把原始函数替换为装饰器返回的新函数（通常是 wrapper）
这个 wrapper 函数必须能够接收和转发所有传给原函数的参数，这样才能保持原函数的行为不变。

而我们对wrapper函数的定义 是使用*args **kwargs 所以传入的参数 会被处理-args 将不定长位置参数处理为元组() **kwargs 将不定长的关键字参数处理为字典{}
在wrapper函数内部，我们会正常调用func()，对于不是定义的函数中使用*   ** 实际上就是解包，所以func会重新接收位置参数，和关键字参数，按照
打包时的顺序，所以func()本身需要的参数不会收到影响
至于几个嵌套函数的执行流程-可参考如上的描述，了解嵌套函数的执行过程，以及闭包的原理，即可理解

其实为什么被装饰函数的参数会传递给wrapper呢，我个人理解其实抛开参数本身来说
对于func使用装饰器后，其实返回的是wrapper，也就是说你最后触发的是wrapper
所以参数也就被传递给wrapper了

而为什么写一个生成装饰器的函数是要使用@最外层函数的名称呢，其实还是嵌套函数本身的逻辑
最外层函数
@time(1)--看下time函数的写法，其实time函数本身返回的是一个装饰器函数，所以本质上还是和我们之前
简单装饰器的原理一致，所以一定要看最外层函数的返回，这种写法相当于给外面包了一层，使用闭包原则，让内层函数
可以使用外层函数的变量

"""

# import time

# # 简单的装饰器
# def decorator(func):
#     def wrapper(*args, **kwargs):
#         print("开始")
#         start_time = time.time()
#         result = func()
#         end_time = time.time()
#         print("结束")
#         print(f"花费时间{end_time - start_time}")
#         return result
#
#     return wrapper
#
#
# @decorator
# def say():
#     time.sleep(1)
#     print(f"hello liming")
#
#
# if __name__ == "__main__":
#     say("liming")


import time


# 含参装饰器，既生成不同装饰器的函数
def count(times):
    def decorator(func):
        def wrapper(*args, **kwargs):
            start_time = time.time()
            result = func(*args, **kwargs)
            end_time = time.time()
            if end_time - start_time > times:
                print(f"{func.__name__} took longer than {times}")
            else:
                print(f"{func.__name__} took shorter than {times}")
            return result

        return wrapper

    return decorator


@count(1)
def say_1(name):
    time.sleep(2)
    print(f"hello, {name}")


@count(0.5)
def say_2():
    time.sleep(0.2)
    print("hello")


if __name__ == "__main__":
    say_1("liming")
    print(say_1.__name__)
    say_2()
    print(say_2.__name__)
